package agent;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

import oracle.MoveTypes;
import oracle.Oracle;
import oracle.OracleMove;
import oracle.OracleTile;
import agent.strategy.BoardMemory;
import agent.strategy.GreedyStrategy;
import agent.strategy.Strategy;
import agent.strategy.StrategyManager;
import agent.strategy.nextDirObject;

/**
 * The Agent class allows us to create multiple Agent types that can move through the world
 * with potentially different policies. The StrategyManager has associated strategies it will listen
 * to. The likelihood of listening to each strategy is defined by the agent, so when the manager is
 * queried for advice, we'll get something of the flavor of this agent.
 * 
 * The basic functionality of an agent is only the ability to "play the game". 
 * In order to do this, an agent must implement it's movement style.
 * 
 * @author adam
 *
 */
public class Agent
{
	/** The oracle this agent interacts with. It will tell us everything about the world as we discover it */
	public Oracle oracle;
	
	/** The total number of rounds in a game */
	public static final int TOTAL_ROUNDS = 10;
	/** The total number of runs in a round */
	public static final int TOTAL_RUNS = 10;
	
	// Fields necessary for logging everything that our agent does
	private FileWriter fileWriter;
	/** The file writer for the log that we will be recording to */
	private BufferedWriter logWriter;
	/** The titles that will appear at the top of a csv file if we want to view our log */
	public static final String logTitles = "round,run,movesRemaining,attemptedMove,madeMove,location,madeMoveScore,score";
	/** A log of all movements made by the tutor. Each element is a row/movement and all fields are comma separated */
	
	// Fields necessary for storing information about what we're doing
	/**
	 * The world is expressed as a 3D array of AgentTiles. Each element begins as null, but if we explore a tile
	 * in a given round, then we define its spot in the array. For example, if we visit 3,4 in round 7, it would exist
	 * at world[3][4][7]
	 */
	private AgentTile[][][] world;
	/** Our world is tracked by the BoardMemory */
	private BoardMemory boardMemory;
	/** A round is a unique world */
	public int currentRound;
	/** A run is one attempt through a given round */
	public int currentRun;
	/** The location we're currently at */
	public AgentTile currentPosition;
	/** Number of moves before the run ends */
	private int movesRemaining;
	/** The score we've earned so far in the game */
	private double score;
	
	// Fields necessary for deciding what to do
	private StrategyManager strategyManager;
	
	/**
	 * This fills in these fields and tells what the Agents belief is about them
	 * @param walls		True if there is a wall in that location
	 * @param activeTerminals	true if there is a terminal guaranteed in that position
	 * @param pastTerminals		true if there was ever a terminal in that location
	 * @param exploredActive	Becomes true if we have explored a tile this round, (know its value)
	 * @param exploredPast		Becomes true if we have explored a tile in the past
	 */
	public void fillWallsAndTerminals(boolean walls[][], boolean activeTerminals[][], boolean pastTerminals[][],
			boolean exploredActive[][], boolean exploredPast[][]){
		int myRound = oracle.getRoundNumber();
		for(int i=0; i<= myRound; i++){
			for(int r=0; r<Oracle.MAX_ROW; r++){
				for(int c=0; c<Oracle.MAX_COL; c++){
					if(world[r][c][i] != null){
						if(world[r][c][i].isWall && i == myRound && walls != null)
							walls[r][c] = true;
						if(world[r][c][i].isTerminalState && pastTerminals != null)
							pastTerminals[r][c] = true;
						if(world[r][c][i].isTerminalState && i == myRound && activeTerminals != null)
							activeTerminals[r][c] = true;
						if(i==myRound && exploredActive != null)
							exploredActive[r][c] = true;
						if(exploredPast != null)
							exploredPast[r][c] = true;
					}
				}
			}
		}
	}
	
	/**
	 * This is going to have to give an estimate on the worth of the world
	 * 
	 * ----------- Currently this is just hackery, we want to used intelligent
	 *             decisions to fill in these tiles
	 * 
	 * @param values This will hold the values of each of the tiles, just as a number
	 */
	public void fillInAccurateValues(double [][] values){
		int myRound = oracle.getRoundNumber();
		
		for(int r=0; r<Oracle.MAX_ROW; r++){
			for(int c=0; c<Oracle.MAX_COL; c++){
				values[r][c] = -3.0;
			}
		}
		
		for(int i=0; i<= myRound; i++){
			for(int r=0; r<Oracle.MAX_ROW; r++){
				for(int c=0; c<Oracle.MAX_COL; c++){
					if(world[r][c][i] != null)
						values[r][c] = world[r][c][i].reward;
				}
			}
		}
	}
	
	/**
	 * This is going to give an estimate of the worth of the world
	 * but it is going to be biased that tiles are more negative
	 * to provide more incentive to moving towards a terminal
	 * @param values This holds our estimate 
	 */
	public void fillInPoorValues(double values[][]){
	int myRound = oracle.getRoundNumber();
		
		for(int r=0; r<Oracle.MAX_ROW; r++){
			for(int c=0; c<Oracle.MAX_COL; c++){
				values[r][c] = -3.2;
			}
		}
		for(int i=0; i<= myRound; i++){
			for(int r=0; r<Oracle.MAX_ROW; r++){
				for(int c=0; c<Oracle.MAX_COL; c++){
					if(world[r][c][i] != null)
						values[r][c] = world[r][c][i].reward - .1;
				}
			}
		}
	}
	
	
	/**
	 * Initialize an Agent with 0 knowledge of the world, a blank log file, and define our strategies with given probabilities/weights.
	 * 
	 * I'm not positive what our strategy parameters are going to be, so I'll just make it this general list
	 */
	public Agent(Oracle o, String logFilePath, StrategyManager strategy)
	{
		// Start up the oracle
		this.oracle = o;
		// Try to open up our log file and write down the titles we'll be using
		try
		{
			this.fileWriter = new FileWriter(logFilePath);
			this.logWriter = new BufferedWriter(fileWriter);
			this.logWriter.write(Agent.logTitles);
			this.logWriter.newLine();
		} catch (IOException e)
		{
			System.err.println("Couldn't open the log file at: " + logFilePath);
		}
		
		// Define our straight forward fields
		this.currentRound = 0;
		this.currentRun = 0;
		this.movesRemaining = oracle.getNumMovesLeftInRound();
		this.score = oracle.getCurrRoundScore();
		
		int worldRowDimension = Oracle.MAX_ROW;
		int worldColDimension = Oracle.MAX_COL;
		 
		// Initialize the world. Example: A 10x10 world with 5 rounds would be an AgentTile[10][10][5]
		// TODO: Delete this to make way for the BoardMemory
		this.world = new AgentTile[worldRowDimension][worldColDimension][Agent.TOTAL_RUNS];
		for (int row = 0; row < worldRowDimension; row++)
		{
			for (int col = 0; col < worldColDimension; col++)
			{
				for (int round = 0; round < Agent.TOTAL_RUNS; round++)
				{
					world[row][col][round] = null;
				}
			}
		}
		
		// Start up our BoardMemory with the size of the world
		this.boardMemory = new BoardMemory(Oracle.MAX_ROW, Oracle.MAX_COL);
		
		// Define our current position and add that knowledge to our world
		this.currentPosition = this.convertOracleTileToAgentTile(oracle.getCurrentTile());
		this.observeAgentTile(currentPosition);
		this.boardMemory.addTile(currentPosition);
		
		// Define our StrategyManager with ourself (i.e., everything we know about the world)
		this.strategyManager = strategy;
		this.strategyManager.setAgent(this);
		this.strategyManager.defineStrategies();
	}
	
	/**
	 * This method converts a MoveObject to an AgentMove. I don't think we would ever need to do the reverse conversion.
	 * 
	 * @param move - The MoveObject we're converting 
	 * @return The new AgentMove
	 */
	protected AgentMove convertOracleMoveToAgentMove(OracleMove move)
	{
		/* something like attmepting a move when the user is dead */
		if(move == null){
			System.out.println("move is null");
			return null;
		}
		AgentTile newLocation = this.convertOracleTileToAgentTile(move.newLocTile);
		if(newLocation == null){
			System.out.println("new location is null");
		}
		AgentMove agentMove = new AgentMove(move.movePerformed, newLocation.reward, newLocation);
		
		return agentMove;
	}
	
	protected AgentTile convertOracleTileToAgentTile(OracleTile tile)
	{
		if(tile == null){
			System.out.println("tile is null");
		}
		AgentTile agentTile = new AgentTile(tile.row, tile.column, tile.value);
		agentTile.isTerminalState = tile.isTerminalState;
		agentTile.isWall = tile.isWall;
		agentTile.isVisited = tile.isVisited;
		
		return agentTile;
	}
	
	/**
	 * @return The BoardMemory that keeps track of this agent's world
	 */
	public BoardMemory getBoardMemory()
	{
		return this.boardMemory;
	}
	
	/**
	 * Record each move that we make 
	 * @param moveAttempted
	 * @param moveMade
	 */
	private void logMovement(int moveAttempted, AgentMove moveMade)
	{
		try
		{
			// This is mostly just a dump of Agent fields. This will need to be updated if we add more information
			String logRow = "" + this.oracle.getRoundNumber();
			logRow += "," + this.oracle.getRunNumber();
			logRow += "," + this.oracle.getNumMovesLeftInRound();
			logRow += "," + MoveTypes.toString(moveAttempted);
			logRow += "," + MoveTypes.toString(moveMade.getMoveMade());
			logRow += "," + this.oracle.currLocation.row + "-" + this.oracle.currLocation.column;
			logRow += "," + moveMade.getMoveScore();
			logRow += "," + this.score;
			this.logWriter.write(logRow);
			this.logWriter.newLine();
		} catch (IOException e)
		{
			System.err.println("Could not print row to log file");
		}
	}
	
	/**
	 * Loop through the oracle and add all tiles in the board memory that are visible
	 */
	public void lookAround()
	{
		for (int row = 0; row < this.oracle.MAX_ROW; row++)
		{
			for (int col = 0; col < this.oracle.MAX_COL; col++) 
			{
				OracleTile tile = this.oracle.getTileByPosition(row, col);
				if (tile != null)
				{
					AgentTile agentTile = this.convertOracleTileToAgentTile(tile);
					this.boardMemory.addTile(agentTile);
				}
			}
		}
	}
	
	/**
	 * Just like play, but it will return an upper bound on our maximum expected score. Because of random moves,
	 * this score will not always be consistent with each run through, but the agent is omnipotent and can see all
	 * squares, so it should be as good as possible.
	 * 
	 * Godmode does not use a StrategyManager BECAUSE IT IS THE STRATEGY MANAGER.
	 * 
	 * @return A score worthy of the gods.
	 */
	public double godMode()
	{
		this.currentPosition = this.convertOracleTileToAgentTile(oracle.getCurrentTile());
        Strategy myGreed = new GreedyStrategy(null);
        myGreed.setAgent(this);
        myGreed.setUpFields();
        AgentMove madeMove = null;
        // Play for the designated number of rounds
        for (; this.currentRound < Agent.TOTAL_ROUNDS; this.currentRound++)
        {
        	while(oracle.getRunNumber() < 10){
        		System.out.println("************************************   RUN " + (oracle.getRunNumber()+1));
        		while(this.oracle.isPlayable()){
        			nextDirObject nextR = myGreed.nextMove(oracle.getNumMovesLeftInRound(), 0, 1);
        			/*if(oracle.getRoundNumber() < 6)
                                        nextR.moveDirection = MoveTypes.KILL_SELF;
                                */
        			this.updateEntireBoard();
        			this.currentPosition = this.convertOracleTileToAgentTile(oracle.getCurrentTile());
        			madeMove = this.convertOracleMoveToAgentMove(this.oracle.makeMove(nextR.moveDirection));
        			this.observeAgentTile(this.currentPosition); // Record our knowledge from this movement
        			this.currentPosition = this.convertOracleTileToAgentTile(oracle.getCurrentTile());
        			this.score = oracle.getCurrRoundScore(); // Let the oracle be in charge of score keeping
        			this.updateEntireBoard();
        			System.out.println(oracle.getCurrMoveNumber() + "  "+ MoveTypes.toString(nextR.moveDirection) + " score::" + this.score
        					+ "  ("+this.currentPosition.row+","+ this.currentPosition.column + ") " +this.currentPosition.reward );
        			if (this.currentPosition.isTerminalState)
        			{
        				System.out.println("+++++++++++++ TERMINAL ++++++++++++++++");
        			}
        		}
        		System.out.println("SCORE :: " + oracle.getCurrRoundScore());
        		oracle.incrementRunNumber();
        		
        	}
        	oracle.incrementRoundNumber();
        	oracle.useGodMode();
        }
        return oracle.getCurrRoundScore();
	}
	
	/**
	 * This method will place an AgentTile into our world of known knowledge
	 * 
	 * @param tile The tile we're currently observing
	 */
	private void observeAgentTile(AgentTile tile)
	{
		tile.isVisited = true;
		tile.runSeen = this.currentRun;
		tile.moveCountWhenExplored = this.movesRemaining;
		
		world[tile.row][tile.column][this.currentRound] = tile.deepCopy(); 
	}
	
	/**
	 * This method will return the move the agent wants to attempt to make
	 * There is a lot more to this, but for now we'll keep it stupid. Actual thought will come soon.
	 * 
	 * @return The move that the agent will attempt to make. This is a code from MoveTypes
	 */
	private int planMove()
	{
		return this.strategyManager.recommendMove();
	}
	
	/**
	 * The play method will play through all runs of all rounds and then return the agent's final score.
	 * 
	 * @return The agent's final score
	 */
	public double play()
	{
		System.out.println("PLAY " + oracle.getCurrentTile().toString());
		double score = 0;
		this.currentPosition = this.convertOracleTileToAgentTile(oracle.getCurrentTile());
		
		// Play for the designated number of rounds
		for (; this.currentRound < Agent.TOTAL_ROUNDS; this.currentRound++)
		{
			System.out.println("ROUND "+ (1 + oracle.getRoundNumber()));
			try
			{ // Load the board for each new round
				if (this.currentRound != 0)
					oracle.initBoardInputs();
			} catch (Exception e)
			{
				System.err.println("Couldn't initialize the board " + oracle.getRoundNumber());
				e.printStackTrace();
				System.exit(0); // Bail if we can't get the board
			}
			
			// The number of moves decays per run, so check what it is this time
			this.movesRemaining = oracle.getNumMovesLeftInRound();
			
			// The explore move doesn't decrement the move counter, so let the Oracle handle tracking the number of moves left
			while (oracle.getRunNumber() < 10){
				System.out.println("RUN " + (oracle.getRunNumber()+1));
				while (this.oracle.isPlayable())
				{
					// use the round one strategy
					int plannedMove;
					if (!oracle.isUserAlive)
					{
						this.score = oracle.getCurrRoundScore();
						break;
					} else
					{
						plannedMove = this.planMove();
					}
					System.out.println(MoveTypes.toString(plannedMove));
					AgentMove madeMove = this.convertOracleMoveToAgentMove(this.oracle.makeMove(plannedMove));
					if (madeMove != null)
					{
						this.logMovement(plannedMove, madeMove);
					} else
					{ // our move might be null because we are dead. try to log that
						if (!this.oracle.isUserAlive)
						{
							AgentMove am = new AgentMove(MoveTypes.KILL_SELF, this.oracle.KILL_SELF_PENALTY, this.currentPosition);
							this.logMovement(plannedMove, am);
						} else
						{
							AgentMove am = new AgentMove(MoveTypes.NO_MOVE, this.currentPosition.reward, this.currentPosition);
							this.logMovement(plannedMove, am);
						}
					}
					if (madeMove.getMoveMade() == MoveTypes.LOOK_AROUND)
					{
						this.lookAround();
					}
					this.currentPosition = this.convertOracleTileToAgentTile(oracle.getCurrentTile());
					this.observeAgentTile(this.currentPosition); // Record our knowledge from this movement
					this.boardMemory.addTile(this.currentPosition); // Record to the boardMemory
					this.score = oracle.getCurrRoundScore(); // Let the oracle be in charge of score keeping
					this.updateEntireBoard();
				}
				this.oracle.incrementRunNumber();
				System.out.println("%%%%%%%%%%%%%%score%%%%%%%%%%%%%%");
				System.out.println("score: " + this.score);
			}
			this.oracle.incrementRoundNumber(); // tell the oracle we're at the next round so the newly mutated map can be loaded
		}
		
		// Shut down the logger and return our score
		try
		{
			this.logWriter.close();	
		} catch (IOException e)
		{
			System.err.println("Error closing the log file");
		}
		score = oracle.getTotalScore();
		return score;
	}
	
	/**
	 * This is to be called after a LOOK_AROUND action
	 * More tiles are visible and we need to mark that they are visible.
	 */
	private void updateEntireBoard() {
		int roundNum = oracle.getRoundNumber();
		for(int r=0; r< Oracle.MAX_ROW; r++){
			for(int c=0; c< Oracle.MAX_COL; c++){
				OracleTile myT = oracle.getTileByPosition(r, c);
				if(myT != null)
					world[r][c][roundNum] = this.convertOracleTileToAgentTile(myT);
			}
		}
	}

	/**
	 * The Agent's toString will report back...stuff. TODO: Make a pretty report feature
	 */
	public String toString()
	{
		return "" + this.score; // For now just the score
	}
}